﻿// <copyright file="CommandLine.cs" company="Tom Ballard">
// Copyright (c) Tom Ballard.  All rights reserved.
// </copyright>

namespace XmlTool
{
    #region Using directives
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using XmlTool.Properties;
    #endregion

    /// <summary>
    /// Command line helper.
    /// </summary>
    internal class CommandLine
    {
        #region Fields
        /// <summary>
        /// The success result.
        /// </summary><![CDATA[]]>
        public const int SuccessResult = 0;

        /// <summary>
        /// The error result.
        /// </summary>
        public const int ErrorResult = 1;

        /// <summary>
        /// The show help result.
        /// </summary>
        public const int ShowHelpResult = 2;

        /// <summary>
        /// The flag.
        /// </summary>
        private Dictionary<string, Tuple<int, int>> flag;
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="CommandLine" /> class.
        /// </summary>
        /// <param name="args">The arguments.</param>
        public CommandLine(string[] args)
        {
            this.List = Parse(args).ToArray();
            this.Option = this.List.Select((s, i) => new { s = s, i = i, })
                .Where(x => !string.IsNullOrEmpty(x.s) && (1 < x.s.Length) && (x.s[0] == '-')).Select(x => x.i).ToArray();
            this.flag = new Dictionary<string, Tuple<int, int>>();
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the list.
        /// </summary>
        /// <value>
        /// The list.
        /// </value>
        public IReadOnlyList<string> List { get; private set; }

        /// <summary>
        /// Gets the option.
        /// </summary>
        /// <value>
        /// The option.
        /// </value>
        public IReadOnlyList<int> Option { get; private set; }

        /// <summary>
        /// Gets the file.
        /// </summary>
        /// <value>
        /// The file.
        /// </value>
        public IEnumerable<string> File
        {
            get
            {
                var a = (this.flag.Count == 0) ? (int[])this.Option : this.Option
                    .Concat(this.flag.Values.SelectMany(x => Enumerable.Range(x.Item1, x.Item2)))
                    .OrderBy(i => i).ToArray();
                var file = Enumerable.Range(0, this.List.Count)
                    .Where(i => Array.BinarySearch(a, i) < 0).Select(i => this.List[i]);
                return file;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether [show debug].
        /// </summary>
        /// <value>
        /// <see langword="true" /> if [show debug]; otherwise, <see langword="false" />.
        /// </value>
        private static bool ShowDebug { get; set; }
        #endregion

        #region Methods
        /// <summary>
        /// Runs the specified action.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <returns>The return value.</returns>
        public static int Run(Func<int> action)
        {
            if (object.ReferenceEquals(action, null))
            {
                throw new ArgumentNullException("action");
            }

            int returnValue;
            try
            {
                returnValue = action();
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ShowDebug ? ex.ToString() : ex.Message);
                returnValue = ErrorResult;
            }

            if (Debugger.IsAttached)
            {
                Console.WriteLine(CommandLineStrings.PressEnterToEndProgram);
                Console.ReadLine();
            }

            return returnValue;
        }

        /// <summary>
        /// Globs the specified wildcard.
        /// </summary>
        /// <param name="wildcards">The wildcards.</param>
        /// <param name="search">The search.</param>
        /// <returns>The file system entries.</returns>
        public static IEnumerable<string> Glob(IEnumerable<string> wildcards, Func<string, string, IEnumerable<string>> search)
        {
            if (object.ReferenceEquals(search, null))
            {
                throw new ArgumentNullException("search");
            }

            foreach (string wildcard in wildcards)
            {
                string dir = Path.GetDirectoryName(wildcard);
                if (string.IsNullOrEmpty(dir))
                {
                    dir = ".";
                }

                string file = Path.GetFileName(wildcard);
                var match = search(dir, file);
                foreach (var e in match)
                {
                    yield return e;
                }
            }

            yield break;
        }

        /// <summary>
        /// Determines whether [is standard io] [the specified file].
        /// </summary>
        /// <param name="file">The file.</param>
        /// <returns><see langword="true" /> if [is standard io]; otherwise, <see langword="false" />.</returns>
        public static bool IsStandardIO(string file)
        {
            bool result = string.Equals(file, "-", StringComparison.Ordinal);
            return result;
        }

        /// <summary>
        /// Uses the reader.
        /// </summary>
        /// <param name="file">The file.</param>
        /// <param name="action">The action.</param>
        public static void UseReader(string file, Action<TextReader> action)
        {
            if (object.ReferenceEquals(action, null))
            {
                throw new ArgumentNullException("action");
            }

            if (IsStandardIO(file))
            {
                action(Console.In);
            }
            else
            {
                using (var reader = new StreamReader(file))
                {
                    action(reader);
                }
            }
        }

        /// <summary>
        /// Uses the writer.
        /// </summary>
        /// <param name="file">The file.</param>
        /// <param name="action">The action.</param>
        public static void UseWriter(string file, Action<TextWriter> action)
        {
            if (object.ReferenceEquals(action, null))
            {
                throw new ArgumentNullException("action");
            }

            if (IsStandardIO(file))
            {
                action(Console.Out);
            }
            else
            {
                using (var writer = new StreamWriter(file))
                {
                    action(writer);
                }
            }
        }

        /// <summary>
        /// Handles the debug.
        /// </summary>
        /// <returns><see langword="true" /> if handled; otherwise, <see langword="false" />.</returns>
        public static bool HandleDebug()
        {
            if (ShowDebug)
            {
                return false;
            }

            ShowDebug = true;
            return true;
        }

        /// <summary>
        /// Shows the help.
        /// </summary>
        /// <param name="action">The action.</param>
        public static void ShowHelp(Action<TextWriter, string> action)
        {
            if (object.ReferenceEquals(action, null))
            {
                throw new ArgumentNullException("action");
            }

            var err = Console.Error;
            err.WriteLine("Usage:");
            var assembly = typeof(Program).Assembly;
            var exe = Path.GetFileNameWithoutExtension(assembly.Location);
            action(err, exe);
            var version = (AssemblyFileVersionAttribute)assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute)).Single();
            err.WriteLine("Version: {0}", version.Version);
        }

        /// <summary>
        /// Shows the option error.
        /// </summary>
        /// <param name="index">The index.</param>
        public void ShowOptionError(int index)
        {
            if ((index < 0) || (this.List.Count < index))
            {
                throw new ArgumentOutOfRangeException("index");
            }

            Console.Error.WriteLine(CommandLineStrings.OptionError, this.List[index]);
        }

        /// <summary>
        /// Adds the flag.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="start">The start.</param>
        /// <param name="count">The count.</param>
        /// <returns><see langword="true" /> if the flags are available; otherwise, <see langword="false" />.</returns>
        public bool AddFlag(string key, int start, int count)
        {
            if (object.ReferenceEquals(key, null))
            {
                throw new ArgumentNullException("key");
            }
            else if (start < 0)
            {
                throw new ArgumentOutOfRangeException("start");
            }
            else if (count <= 0)
            {
                throw new ArgumentOutOfRangeException("count");
            }
            else if (this.flag.ContainsKey(key))
            {
                return false;
            }
            else if (this.List.Count <= (start + count))
            {
                return false;
            }
            else if (this.Option.Intersect(Enumerable.Range(start, count)).Any())
            {
                return false;
            }

            this.flag.Add(key, new Tuple<int, int>(start, count));
            return true;
        }

        /// <summary>
        /// Parses the specified arguments.
        /// </summary>
        /// <param name="args">The arguments.</param>
        /// <returns>The parsed arguments.</returns>
        private static IEnumerable<string> Parse(string[] args)
        {
            if (object.ReferenceEquals(args, null))
            {
                throw new ArgumentNullException("args");
            }

            foreach (string arg in args)
            {
                if (string.IsNullOrEmpty(arg) ||
                    (arg[0] != '-') ||
                    IsStandardIO(arg) ||
                    ((2 < arg.Length) && (arg[1] == '-')))
                {
                    yield return arg;
                    continue;
                }

                for (int i = 1; i < arg.Length; i++)
                {
                    yield return "-" + arg[i];
                }
            }
        }
        #endregion
    }
}
